using System.Collections.Generic;
using System.Linq;
using Godot;

namespace Arcane.Source.UI
{
    [GlobalClass]
    public partial class ScrollGridContainer : Container
    {
        private int _col;
        [Export]
        public int Column
        {
            get => _col;
            set
            {
                _col = value;
            }
        }
        
        private readonly Stack<Control> _pool = new();
        private float _offset = 0;
        private float _cellSize;
        private float _contentHeight;
        public float ContentHeight => _contentHeight;
        public int ElementCount => _adapter?.GetCount() ?? 0;

        private IAdapter _adapter;

        [Signal]
        public delegate void OffsetChangedEventHandler(double offset);

        public override void _Notification(int what)
        {
            if (what == NotificationResized)
            {
                OnResize();
                foreach (var child in GetChildren())
                {
                    _pool.Push((Control)child);
                    RemoveChild(child);
                }
                GenerateAll();
            }

            if (what == NotificationChildOrderChanged)
            {
                FitChildren();
            }
        }

        private bool _dragging = false;
        private float _velocity = 0;

        public override void _Process(double delta)
        {
            if (Mathf.Abs(_velocity) < 1)
            {
                SetProcess(false);
                _velocity = 0;
            }

            Scroll(_velocity);
            _velocity = Mathf.Lerp(_velocity, 0, 0.1f);
        }

        public override void _Input(InputEvent e)
        {
            if (e is InputEventMouseButton mbe && !mbe.Pressed)
            {
                _dragging = false;
                SetProcess(true);
            }

            if (e is InputEventMouseMotion mme && _dragging)
            {
                Scroll(-mme.Relative.Y);
                _velocity = -mme.Relative.Y;
            }
        }

        public override void _GuiInput(InputEvent e)
        {
            if (e is InputEventMouseButton mbe)
            {
                if (mbe.ButtonIndex == MouseButton.WheelUp)
                {
                    Scroll(-0.2f * _cellSize);
                }
                else if (mbe.ButtonIndex == MouseButton.WheelDown)
                {
                    Scroll(0.2f * _cellSize);
                }
                else if (mbe.ButtonIndex == MouseButton.Left)
                {
                    if (mbe.Pressed)
                    {
                        _dragging = true;
                        SetProcess(false);
                    }
                }
            }         
        }

        public void SetAdapter(IAdapter adapter)
        {
            _adapter = adapter;
            foreach (var child in GetChildren()) child.QueueFree();
            while (_pool.Count > 0) _pool.Pop().QueueFree();

            _offset = 0;

            OnResize();
            GenerateAll();
        }

        public void OnResize()
        {
            var newCellSize = GetRect().Size.X / _col;
            if (_cellSize != 0) _offset *= newCellSize / _cellSize;
            _cellSize = newCellSize;
            _contentHeight = Mathf.CeilToInt((double)ElementCount / _col) * _cellSize;
        }

        public void GenerateAll()
        {
            if (ElementCount == 0) return;
            _offset = _contentHeight <= GetRect().Size.Y ? 0 : Mathf.Clamp(_offset, 0, _contentHeight - GetRect().Size.Y);

            var low = Mathf.FloorToInt(_offset / _cellSize);
            var high = Mathf.CeilToInt((_offset + GetRect().Size.Y) / _cellSize) + 1;
            var begin = low * _col;
            var capacity = (high - low) * _col;
            var count = Mathf.Clamp(_adapter.GetCount() - begin, 0, capacity);
            var yStart = low * _cellSize - _offset;

            for (int i = 0; i < count; i++)
            {
                var c = i % _col;
                var r = i / _col;

                var ele = _pool.Count > 0 ? _pool.Pop() : _adapter.CreateElement();
                _adapter.BindElement(ele, begin + i);
                AddChild(ele);
                FitChildInRect(ele, new(c * _cellSize, r * _cellSize + yStart, new(_cellSize, _cellSize)));
            }
        }

        public void Scroll(float delta) => Navigate(_offset + delta);

        public void Navigate(float offset)
        {
            NavigateNoSignal(offset);
            EmitSignal(SignalName.OffsetChanged, _offset);
        }

        public void NavigateNoSignal(float offset)
        {
            offset = _contentHeight <= GetRect().Size.Y ? 0 : Mathf.Clamp(offset, 0, _contentHeight - GetRect().Size.Y);

            var delta = offset - _offset;
            var prevLow = Mathf.FloorToInt(_offset / _cellSize);
            _offset = offset;
            var low = Mathf.FloorToInt(_offset / _cellSize);

            if (prevLow == low)
            {
                foreach (var child in GetChildren().Cast<Control>())
                {
                    child.Position -= new Vector2(0, delta);
                }
                return;
            }

            // TODO: Optimize this instead of clear all chlid and relayout them.

            var high = Mathf.CeilToInt((_offset + GetRect().Size.Y) / _cellSize) + 1;
            var begin = low * _col;
            var capacity = (high - low) * _col;
            var count = Mathf.Clamp(_adapter.GetCount() - begin, 0, capacity);
            var yStart = low * _cellSize - _offset;
            
            foreach (var child in GetChildren().Cast<Control>())
            {
                _pool.Push(child);
                RemoveChild(child);
            }

            for (int i = 0; i < count; i++)
            {
                var c = i % _col;
                var r = i / _col;

                var ele = _pool.Count > 0 ? _pool.Pop() : _adapter.CreateElement();
                _adapter.BindElement(ele, begin + i);
                AddChild(ele);
                FitChildInRect(ele, new(c * _cellSize, r * _cellSize + yStart, new(_cellSize, _cellSize)));
            }
        }

        public void FitChildren()
        {
            var yStart = Mathf.FloorToInt(_offset / _cellSize) * _cellSize - _offset;
            int i = 0;
            foreach (var child in GetChildren().Cast<Control>())
            {
                var c = i % _col;
                var r = i / _col;

                FitChildInRect(child, new(c * _cellSize, r * _cellSize + yStart, new(_cellSize, _cellSize)));
                i += 1;
            }
        }
    }
}